/* Console.cpp
 Michael Zahniser, 2008
 
 Function definitions for the CursesConsole class.
 */

#include "CursesConsole.h"

#include "Command.h"
#include "TransactForm.h"

#include <cstdlib>
#include <fstream>
#include <sstream>
#include <ncurses.h>
#include <sstream>

using std::getenv;
using std::ifstream;
using std::ofstream;
using std::istringstream;
using std::ostringstream;



// Default constructor:
// When a console is constructed, the database is loaded automatically.
CursesConsole::CursesConsole()
{
    const char *home = getenv("HOME");
    if(!home)
		return;
	
    databasePath = home + string("/.finances");
    ifstream in(databasePath.c_str());
    
    database.Load(in);
}



// When the console is destructed, save the modified data.
CursesConsole::~CursesConsole()
{
    if(!databasePath.empty())
    {
		ofstream out(databasePath.c_str());
		
		database.Save(out);
    }
}



// Run the console.
void CursesConsole::Run()
{
    initscr();
    cbreak();
    keypad(stdscr, true);
    noecho();
	
    // Reserve the last row for user input of commands.
    getmaxyx(stdscr, rows, cols);
    --rows;
	
    while(true)
    {
		treeIt = DrawTree();
		mvaddstr(rows, 0, line.c_str());
		clrtoeol();
		int key = getch();
		if(HandleTree(key))
			continue;
		if(!HandleCommands(key))
			break;
    }
    
    endwin();
}



TextTree::Iterator CursesConsole::DrawTree()
{
    TextTree::Iterator it(tree);
    TextTree::Iterator selected;
	
    int y = -scrollRow;
    for( ; it; ++it, ++y)
    {
		if((y >= 0) & (y < rows))
		{
			bool isSelected = (y + scrollRow == selectedRow);
			if(isSelected)
				attron(A_REVERSE);
			mvaddstr(y, 0, it->c_str());
			clrtoeol();
			if(isSelected)
			{
				attroff(A_REVERSE);
				selected = it;
			}
		}
    }
    for( ; y < rows; ++y)
    {
		move(y, 0);
		clrtoeol();
    }
	
    return selected;
}



// Handle tree-navigation events.
// Return false if the given key is not for navigation.
bool CursesConsole::HandleTree(int key)
{		
    if(key == KEY_UP)
    {
		if(selectedRow > 0)
			--selectedRow;
    }
    else if(key == KEY_DOWN)
    {
		if(treeIt)
			++selectedRow;
    }
    else if(key == KEY_RIGHT)
    {
		if(treeIt)
			treeIt.Expand();
    }
    else if(key == KEY_LEFT)
    {
		if(treeIt)
			treeIt.Collapse();
    }
    else if(key == KEY_NPAGE)
    {
		for( ; selectedRow < scrollRow + (rows - 1); ++selectedRow)
			++treeIt;
		for(int i = 0; i < rows - 5 && treeIt; ++i, ++treeIt)
			++selectedRow;
    }
    else if(key == KEY_PPAGE)
    {
		selectedRow = scrollRow - (rows - 5);
		if(selectedRow < 0)
			selectedRow = 0;
    }
    else
		return false;
	
    if(scrollRow > selectedRow)
		scrollRow = selectedRow;
    else if(scrollRow + (rows - 1) < selectedRow)
		scrollRow = selectedRow - (rows - 1);
	
    return true;
}



// Handle text entry events. Return false if the user enters
// a quit command.
bool CursesConsole::HandleCommands(int key)
{
    if((key == '\r') | (key == '\n'))
    {
		Command command(line);
		ClearScreen();
		
		switch(command.GetType())
		{
			case Command::QUIT:
				return false;
				
			case Command::BALANCE:
				DoBalance(command);
				break;
				
			case Command::CHECKS:
				DoChecks();
				break;
				
			case Command::INFO:
				DoInfo(command);
				break;
				
			case Command::RECORDS:
				DoRecords(command);
				break;
				
			case Command::TRANSACT:
				ShowText(TransactForm(database).Run());
				break;
				
			case Command::CANCEL:
				ShowText(database.CancelTransaction());
				break;
				
			case Command::SET:
				ShowText(database.SetFlag(
					command.GetName(), command.GetArgument(), true));
				break;
				
			case Command::CLEAR:
				ShowText(database.SetFlag(
					command.GetName(), command.GetArgument(), false));
				break;
				
			case Command::TYPE:
				ShowText(
					database.SetType(command.GetName(), command.GetArgument()));
				break;
				
			case Command::MAKE:
				ShowText(
					database.AddAccount(command.GetName()));
				break;
				
			default:
				break;
		}
		
		DrawTree();
    }
    else if(key == KEY_BACKSPACE || key == 127)
    {
		if(!line.empty())
			line.erase(line.end() - 1, line.end());
    }
    else if(key < 128)
		line += static_cast<char>(key);
	
    return true;
}



// Add the given string to the tree, breaking and indenting at
// every '\n' in the string.
void CursesConsole::ShowText(const string &text)
{
    size_t begin = 0;
    size_t end = begin;
    for(int indent = 0; end != string::npos; ++indent)
    {
		end = text.find('\n', begin);
		tree.Add(indent, text.substr(begin, end));
		begin = end + 1;
    }
}



// Clear the screen (and the tree).
void CursesConsole::ClearScreen()
{
    line.clear();
    
    tree.Clear();
    scrollRow = 0;
    selectedRow = 0;
	
    for(int y = 0; y <= rows; ++y)
    {
		move(y, 0);
		clrtoeol();
    }
}



void CursesConsole::DoBalance(const Command &command)
{
    const map<string, Account> &result = database.Query(command.GetDate());
	
    // We will show percentages relative to this total.
    double total = 0.0;
    ledgers.clear();
    const vector<AccountType> &types = command.GetAccountTypes();
    vector<AccountType>::const_iterator it = types.begin();
    for( ; it != types.end(); ++it)
    {
		AddLedger(result, *it, command);
		// If one of the account types included by the filter is income, the
		// percentages will be displayed relative to that account.
		// Otherwise, percentages will be relative to the first account.
		if(*it == AccountType::IN)
			total = ledgers.back().Total();
    }
	
    // The total is of this account type.
    AccountType totalType = AccountType::IN;
    if(!total)
    {
		total = ledgers.front().Total();
		totalType = types.front();
    }
	
    // Print the ledgers.
    it = types.begin();
    vector<Ledger>::const_iterator ledgerIt = ledgers.begin();
    for( ; it != types.end(); ++it, ++ledgerIt)
    {
		double thisTotal = it->IsCompatible(totalType) ? total : 0.0;
		ledgerIt->Print(tree, thisTotal);
    }    
}



void CursesConsole::DoChecks()
{
    const map<string, Account> &result = database.Checks();
	
    Ledger ledger;
    map<string, Account>::const_iterator it = result.begin();
    for( ; it != result.end(); ++it)
		ledger.Add(it->first, it->second.Balance());
	
    ledger.Print(tree);
}



void CursesConsole::DoInfo(const Command &command)
{
    const map<string, Account> &result = database.Accounts();
    map<string, Account>::const_iterator it = result.begin();
    for( ; it != result.end(); ++it)
    {
		if(!command.MatchesName(it->first))
			continue;
		
		ostringstream out;
		out<<it->first<<"  ";
		it->second.PrintInfo(out);
		tree.Add(0, out.str());
    }
}



void CursesConsole::DoRecords(const Command &command)
{
    Date date = command.GetDate();
    const vector<Transaction> &result = database.Transactions(date);
	
    vector<Transaction>::const_iterator it = result.end();
    while(it != result.begin())
    {
		// Iterate from the last record to the first, to put the most recent on top.
		--it;
		if(!command.MatchesName(it->GetFromAccount()) &&
				!command.MatchesName(it->GetToAccount()))
			continue;
		
		ostringstream out;
		it->PrettyPrint(out);
		string record = out.str();
		size_t pos = record.find('\n');
		tree.Add(0, record.substr(0, pos));
		tree.Add(1, record.substr(pos + 1, string::npos));
    }   
}



// Add to the vector of ledgers.
void CursesConsole::AddLedger(const map<string, Account> &accounts,
							  AccountType accountType,
							  const Command &command)
{
    ledgers.push_back(Ledger());
    map<string, Account>::const_iterator it = accounts.begin();
    for( ; it != accounts.end(); ++it)
    {
		if(it->second.Type() != accountType || !command.MatchesName(it->first))
			continue;
		
		ledgers.back().Add(it->first, it->second.Balance());
    }
}
